﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace StableDiffusionTools.Imaging
{
    public partial class cImage : ICloneable
    {
        #region fields
        // image data
        /// <summary>
        /// An array containing the images' pixel data
        /// </summary>
        private readonly sPixel[] _imageData;
        /// <summary>
        /// The images' width
        /// </summary>
        private readonly int _width;
        /// <summary>
        /// The images' height
        /// </summary>
        private readonly int _height;

        private OutOfBoundsMode _horizontalOutOfBoundsMode;
        private OutOfBoundsUtils.OutOfBoundsHandler _horizontalOutOfBoundsHandler;
        private OutOfBoundsMode _verticalOutOfBoundsMode;
        private OutOfBoundsUtils.OutOfBoundsHandler _verticalOutOfBoundsHandler;
        #endregion

        #region properties
        /// <summary>
        /// Gets the width of the image.
        /// </summary>
        /// <value>The width.</value>
        public int Width
        {
            get
            {
                return (this._width);
            }
        }
        /// <summary>
        /// Gets the height of the image.
        /// </summary>
        /// <value>The height.</value>
        public int Height
        {
            get
            {
                return (this._height);
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the red values only.
        /// </summary>
        /// <value>The greyscale image from the red components.</value>
        public cImage Red
        {
            get
            {
                return (new cImage(this, pixel => pixel.Red));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the green values only.
        /// </summary>
        /// <value>The greyscale image from the green components.</value>
        public cImage Green
        {
            get
            {
                return (new cImage(this, pixel => pixel.Green));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the blue values only.
        /// </summary>
        /// <value>The greyscale image from the blue components.</value>
        public cImage Blue
        {
            get
            {
                return (new cImage(this, pixel => pixel.Blue));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the alpha values only.
        /// </summary>
        /// <value>The greyscale image from the alpha components.</value>
        public cImage Alpha
        {
            get
            {
                return (new cImage(this, pixel => pixel.Alpha));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the luminance values only.
        /// </summary>
        /// <value>The greyscale image from the luminance components.</value>
        public cImage Luminance
        {
            get
            {
                return (new cImage(this, pixel => pixel.Luminance));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the color(U) values only.
        /// </summary>
        /// <value>The greyscale image from the color(U) components.</value>
        public cImage ChrominanceU
        {
            get
            {
                return (new cImage(this, pixel => pixel.ChrominanceU));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the color(V) values only.
        /// </summary>
        /// <value>The greyscale image from the color(V) components.</value>
        public cImage ChrominanceV
        {
            get
            {
                return (new cImage(this, pixel => pixel.ChrominanceV));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the color(u) values only.
        /// </summary>
        /// <value>The greyscale image from the color(u) components.</value>
        public cImage u
        {
            get
            {
                return (new cImage(this, pixel => pixel.u));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the color(v) values only.
        /// </summary>
        /// <value>The greyscale image from the color(v) components.</value>
        public cImage v
        {
            get
            {
                return (new cImage(this, pixel => pixel.v));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the brightness values only.
        /// </summary>
        /// <value>The greyscale image from the brightness components.</value>
        public cImage Brightness
        {
            get
            {
                return (new cImage(this, pixel => pixel.Brightness));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the minimum values only.
        /// </summary>
        /// <value>The greyscale image from the minimum of all components.</value>
        public cImage Min
        {
            get
            {
                return (new cImage(this, pixel => pixel.Minimum));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the maximum values only.
        /// </summary>
        /// <value>The greyscale image from the maximum of all components.</value>
        public cImage Max
        {
            get
            {
                return (new cImage(this, pixel => pixel.Maximum));
            }
        }
        /// <summary>
        /// Extracts the colors from an image and returns a new image with only base colors.
        /// </summary>
        public cImage ExtractColors
        {
            get
            {
                return (new cImage(this, pixel => pixel.ExtractColors));
            }
        }
        /// <summary>
        /// Extracts the grey deltas for use with an image that is color extracted.
        /// </summary>
        public cImage ExtractDeltas
        {
            get
            {
                return (new cImage(this, pixel => pixel.ExtractDeltas));
            }
        }
        /// <summary>
        /// Gets the a new instance containing a greyscale image of the hue values only.
        /// </summary>
        /// <value>The greyscale image from the hue components.</value>
        public cImage Hue
        {
            get
            {
                return (new cImage(this, pixel => pixel.Hue));
            }
        }
        /// <summary>
        /// Gets the a new instance containing an image of the hue values only.
        /// </summary>
        /// <value>The image from the hue components.</value>
        public cImage HueColored
        {
            get
            {
                return (new cImage(this, pixel => {
                    const float conversionFactor = 360f / 256f;
                    var hue = pixel.Hue * conversionFactor;
                    const float saturation = 1f;
                    const float value = 1f;
                    float red, green, blue;

                    if (hue < 0.001)
                    {
                        red = green = blue = 0.5f;
                    }
                    else
                    {
                        hue = hue / 60f;
                        var i = (byte)Math.Floor(hue);
                        var f = hue - i;
                        const float p = value * (1 - saturation);
                        var q = value * (1 - saturation * f);
                        var t = value * (1 - saturation * (1 - f));
                        switch (i)
                        {
                            case 0:
                                {
                                    red = value;
                                    green = t;
                                    blue = p;
                                    break;
                                }
                            case 1:
                                {
                                    red = q;
                                    green = value;
                                    blue = p;
                                    break;
                                }
                            case 2:
                                {
                                    red = p;
                                    green = value;
                                    blue = t;
                                    break;
                                }
                            case 3:
                                {
                                    red = p;
                                    green = q;
                                    blue = value;
                                    break;
                                }
                            case 4:
                                {
                                    red = t;
                                    green = p;
                                    blue = value;
                                    break;
                                }
                            case 5:
                                {
                                    red = value;
                                    green = p;
                                    blue = q;
                                    break;
                                }
                            default:
                                {
                                    throw new NotSupportedException();
                                }
                        }
                    }
                    return (new sPixel(red, green, blue, pixel.Alpha / 255.0));
                }));
            }
        }

        /// <summary>
        /// Gets or sets the horizontal out of bounds mode.
        /// </summary>
        /// <value>
        /// The horizontal out of bounds mode.
        /// </value>
        public OutOfBoundsMode HorizontalOutOfBoundsMode
        {
            get
            {
                return this._horizontalOutOfBoundsMode;
            }
            set
            {
                this._horizontalOutOfBoundsMode = value;
                this._horizontalOutOfBoundsHandler = OutOfBoundsUtils.GetHandlerOrCrash(value);
            }
        }

        /// <summary>
        /// Gets or sets the vertical out of bounds mode.
        /// </summary>
        /// <value>
        /// The vertical out of bounds mode.
        /// </value>
        public OutOfBoundsMode VerticalOutOfBoundsMode
        {
            get
            {
                return this._verticalOutOfBoundsMode;
            }
            set
            {
                this._verticalOutOfBoundsMode = value;
                this._verticalOutOfBoundsHandler = OutOfBoundsUtils.GetHandlerOrCrash(value);
            }
        }
        #endregion
        #region ctor dtor idx
        /// <summary>
        /// Initializes a new instance of the <see cref="cImage"/> class.
        /// </summary>
        /// <param name="width">Width of the image.</param>
        /// <param name="height">Height of the image.</param>
        public cImage(int width, int height)
        {
            this._width = width;
            this._height = height;
            this._imageData = new sPixel[width * height];
            this.HorizontalOutOfBoundsMode = OutOfBoundsMode.ConstantExtension;
            this.VerticalOutOfBoundsMode = OutOfBoundsMode.ConstantExtension;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="cImage"/> class from a given one.
        /// </summary>
        /// <param name="sourceImage">The source image.</param>
        public cImage(cImage sourceImage)
          : this(sourceImage == null ? 0 : sourceImage._width, sourceImage == null ? 0 : sourceImage._height)
        {
            if (sourceImage == null)
                return;

            for (long index = 0; index < sourceImage._imageData.LongLength; index++)
                this._imageData[index] = sourceImage._imageData[index];
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="cImage"/> class by filtering a given one.
        /// </summary>
        /// <param name="sourceImage">The source image.</param>
        /// <param name="filterFunction">The filter.</param>
        public cImage(cImage sourceImage, Func<sPixel, sPixel> filterFunction)
          : this(sourceImage == null ? 0 : sourceImage._width, sourceImage == null ? 0 : sourceImage._height)
        {
            if (sourceImage == null)
                return;

            var width = sourceImage._width;
            Parallel.ForEach(Partitioner.Create(0, this._height), () => 0, (range, _, threadStorage) => {
                for (var y = range.Item2; y > range.Item1;)
                {
                    --y;
                    for (var x = width; x > 0;)
                    {
                        --x;
                        this[x, y] = filterFunction(sourceImage[x, y]);
                    }
                }
                return (threadStorage);
            }, _ => {
            });
        }

        /// <summary>
        /// Initializes a new greyscale instance of the <see cref="cImage"/> class by filtering a given one.
        /// </summary>
        /// <param name="sourceImage">The source image.</param>
        /// <param name="colorFilter">The greyscale filter.</param>
        public cImage(cImage sourceImage, Func<sPixel, byte> colorFilter)
          : this(sourceImage == null ? 0 : sourceImage._width, sourceImage == null ? 0 : sourceImage._height)
        {
            if (sourceImage == null)
                return;

            var width = sourceImage._width;
            Parallel.ForEach(Partitioner.Create(0, this._height), () => 0, (range, _, threadStorage) => {
                for (var y = range.Item2; y > range.Item1;)
                {
                    --y;
                    for (var x = width; x > 0;)
                    {
                        --x;
                        this[x, y] = sPixel.FromGrey(colorFilter(sourceImage[x, y]));
                    }
                }
                return (threadStorage);
            }, _ => {
            });
        }

        /// <summary>
        /// Gets or sets the <see cref="sPixel"/> with the specified X, Y coordinates.
        /// </summary>
        /// <value>The pixel</value>
        public sPixel this[int x, int y]
        {
            get { return this.GetPixel(x, y); }
            set { this.SetPixel(x, y, value); }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal void SetPixel(int x, int y, sPixel value)
        {
            var width = this._width;
            var height = this._height;

            if (x < width && y < height && x >= 0 && y >= 0)
                this._imageData[y * width + x] = value;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal sPixel GetPixel(int x, int y)
        {
            var width = this._width;
            var height = this._height;

            if (x < 0 || x >= width)
                x = this._horizontalOutOfBoundsHandler(x, width, x < 0);

            if (y < 0 || y >= height)
                y = this._verticalOutOfBoundsHandler(y, height, y < 0);

            return (this._imageData[y * width + x]);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal sPixel[] GetImageData()
        {
            return (this._imageData);
        }

        #endregion

        /// <summary>
        /// Stores all available parameterless pixel scalers.
        /// </summary>
        internal static readonly InterpolationMode[] INTERPOLATORS = new[] {
      InterpolationMode.NearestNeighbor,
      InterpolationMode.Bilinear,
      InterpolationMode.Bicubic,
      InterpolationMode.HighQualityBilinear,
      InterpolationMode.HighQualityBicubic,
    };

        /// <summary>
        /// Applies the GDI+ pixel scaler.
        /// </summary>
        /// <param name="type">The type of scaler to use.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="filterRegion">The filter region, if any.</param>
        /// <returns>
        /// The rescaled image.
        /// </returns>
        public cImage ApplyScaler(InterpolationMode type, int width, int height, Rectangle? filterRegion = null)
        {
            if (!((IList<InterpolationMode>)INTERPOLATORS).Contains(type))
                throw new NotSupportedException(string.Format("Interpolation mode '{0}' not supported.", type));

            var startX = filterRegion == null ? 0 : Math.Max(0, filterRegion.Value.Left);
            var startY = filterRegion == null ? 0 : Math.Max(0, filterRegion.Value.Top);

            var endX = filterRegion == null ? this.Width : Math.Min(this.Width, filterRegion.Value.Right);
            var endY = filterRegion == null ? this.Height : Math.Min(this.Height, filterRegion.Value.Bottom);

            // run through scaler
            var bitmap = new Bitmap(width, height);
            using (var graphics = Graphics.FromImage(bitmap))
            {

                //set the resize quality modes to high quality                
                graphics.CompositingQuality = CompositingQuality.HighQuality;
                graphics.InterpolationMode = type;
                graphics.SmoothingMode = SmoothingMode.HighQuality;

                //draw the image into the target bitmap                
                //graphics.DrawImage(source, 0, 0, result.Width, result.Height);

                // FIXME: this is a hack to prevent the microsoft bug from creating a white pixel on top and left border (see http://forums.asp.net/t/1031961.aspx/1)
                graphics.DrawImage(filterRegion == null ? this.ToBitmap() : this.ToBitmap(startX, startY, endX - startX, endY - startY), -1, -1, bitmap.Width + 1, bitmap.Height + 1);
            }
            var result = FromBitmap(bitmap);
            result.HorizontalOutOfBoundsMode = this.HorizontalOutOfBoundsMode;
            result.VerticalOutOfBoundsMode = this.VerticalOutOfBoundsMode;
            return (result);

        }

        /// <summary>
        /// Converts this image to a <see cref="Bitmap"/> instance.
        /// </summary>
        /// <param name="sx">The start x.</param>
        /// <param name="sy">The start y.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <returns>
        /// The <see cref="Bitmap"/> instance
        /// </returns>
        public Bitmap ToBitmap(int sx, int sy, int width, int height)
        {
            var result = new Bitmap(width, height);
            // NOTE: fucking bitmap does not allow parallel writes
            var bitmapData = result.LockBits(
              new Rectangle(0, 0, width, height),
              ImageLockMode.WriteOnly,
              PixelFormat.Format32bppArgb
            );
            var fillBytes = bitmapData.Stride - bitmapData.Width * 4;
            unsafe
            {
                var offset = (byte*)bitmapData.Scan0.ToPointer();
                for (var y = sy; y < sy + height; y++)
                {
                    for (var x = sx; x < sx + width; x++)
                    {
                        var pixel = this[x, y];
                        *(offset + 3) = pixel.Alpha;
                        *(offset + 2) = pixel.Red;
                        *(offset + 1) = pixel.Green;
                        *(offset + 0) = pixel.Blue;
                        offset += 4;
                    }
                    offset += fillBytes;
                }
            }
            result.UnlockBits(bitmapData);
            return (result);
        }

        /// <summary>
        /// Converts this image to a <see cref="Bitmap"/> instance.
        /// </summary>
        /// <returns>The <see cref="Bitmap"/> instance</returns>
        public Bitmap ToBitmap()
        {
            return (ToBitmap(0, 0, this._width, this._height));
        }

        // NOTE: Bitmap objects does not support parallel read-outs blame Microsoft
        /// <summary>
        /// Initializes a new instance of the <see cref="cImage"/> class from a <see cref="Bitmap"/> instance.
        /// </summary>
        /// <param name="bitmap">The bitmap.</param>
        public static cImage FromBitmap(Bitmap bitmap)
        {
            if (bitmap == null)
                return (null);
            var result = new cImage(bitmap.Width, bitmap.Height);

            var height = result._height;
            var width = result._width;

            var bitmapData = bitmap.LockBits(
              new Rectangle(0, 0, width, height),
              ImageLockMode.ReadOnly,
              PixelFormat.Format32bppArgb
            );
            var intFillX = bitmapData.Stride - bitmapData.Width * 4;
            unsafe
            {
                var ptrOffset = (byte*)bitmapData.Scan0.ToPointer();
                for (var y = 0; y < height; y++)
                {
                    for (var x = 0; x < width; x++)
                    {
                        result[x, y] = new sPixel(*(ptrOffset + 2), *(ptrOffset + 1), *(ptrOffset + 0), *(ptrOffset + 3));
                        ptrOffset += 4;
                    }
                    ptrOffset += intFillX;
                }
            }
            bitmap.UnlockBits(bitmapData);

            return (result);
        }

        /// <summary>
        /// Applies the pixel scaler for float32 images.
        /// </summary>
        /// <param name="type">The type of scaler to use.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="centeredGrid">if set to <c>true</c> [centered grid].</param>
        /// <param name="filterRegion">The filter region, if any.</param>
        /// <returns>
        /// The rescaled image.
        /// </returns>
        public cImage ApplyScaler(KernelType type, int width, int height, bool centeredGrid, Rectangle? filterRegion = null)
        {
            var fpImage = FloatImage.FromImage(this, filterRegion);
            var fpResult = fpImage.Resize(width, height, type, centeredGrid);
            var result = fpResult.ToImage();
            return (result);
        }

        /// <summary>
        /// Applies the pixel scaler for float32 images.
        /// </summary>
        /// <param name="type">The type of scaler to use.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="radius">The radius.</param>
        /// <param name="centeredGrid">if set to <c>true</c> [centered grid].</param>
        /// <param name="filterRegion">The filter region, if any.</param>
        /// <returns>
        /// The rescaled image.
        /// </returns>
        public cImage ApplyScaler(int width, int height, float radius, bool centeredGrid, Rectangle? filterRegion = null)
        {
            var fpImage = FloatImage.FromImage(this, filterRegion);
            var fpResult = fpImage.Resize(width, height, radius, centeredGrid);
            var result = fpResult.ToImage();
            return (result);
        }

        /// <summary>
        /// Fills the image with the specified color.
        /// </summary>
        /// <param name="red">The red-value.</param>
        /// <param name="green">The green-value.</param>
        /// <param name="blue">The blue-value.</param>
        /// <param name="alpha">The alpha-value.</param>
        public void Fill(byte red, byte green, byte blue, byte alpha = 255)
        {
            this.Fill(new sPixel(red, green, blue, alpha));
        }
        /// <summary>
        /// Fills the image with the specified pixel.
        /// </summary>
        /// <param name="pixel">The pixel instance.</param>
        public void Fill(sPixel pixel)
        {
            Parallel.For(0, this._imageData.LongLength, offset => this._imageData[offset] = pixel);
        }

        #region ICloneable Members
        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        /// A new object that is a copy of this instance.
        /// </returns>
        public object Clone()
        {
            return (new cImage(this));
        }
        #endregion
    }
}
